/**
* Copyright 2015-2017 The OpenZipkin Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package zipkin.autoconfigure.storage.elasticsearch.aws;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.AWSSessionCredentials;
import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
import com.amazonaws.regions.DefaultAwsRegionProviderChain;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import okhttp3.HttpUrl;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.type.AnnotatedTypeMetadata;
import zipkin.autoconfigure.storage.elasticsearch.http.ZipkinElasticsearchHttpStorageProperties;
import zipkin.storage.StorageComponent;
import zipkin.storage.elasticsearch.http.ElasticsearchHttpStorage;
import static java.lang.String.format;
import static zipkin.internal.Util.checkArgument;
@Configuration
@EnableConfigurationProperties({
ZipkinElasticsearchHttpStorageProperties.class,
ZipkinElasticsearchAwsStorageProperties.class
})
@Conditional(ZipkinElasticsearchAwsStorageAutoConfiguration.AwsMagic.class)
public class ZipkinElasticsearchAwsStorageAutoConfiguration {
static final Pattern AWS_URL =
Pattern.compile("^https://[^.]+\\.([^.]+)\\.es\\.amazonaws\\.com", Pattern.CASE_INSENSITIVE);
static final Logger log =
Logger.getLogger(ZipkinElasticsearchAwsStorageAutoConfiguration.class.getName());
@Bean
@Qualifier("zipkinElasticsearchHttp")
Interceptor awsSignatureVersion4(
ZipkinElasticsearchHttpStorageProperties es,
ZipkinElasticsearchAwsStorageProperties aws,
AWSCredentials.Provider credentials) {
return new AWSSignatureVersion4(region(es, aws), "es", credentials);
}
@Bean String region(ZipkinElasticsearchHttpStorageProperties es,
ZipkinElasticsearchAwsStorageProperties aws) {
List<String> hosts = es.getHosts();
String domain = aws.getDomain();
if (hosts != null && domain != null) {
log.warning(
format("Expected exactly one of hosts or domain: instead saw hosts '%s' and domain '%s'."
+ " Ignoring hosts and proceeding to look for domain. Either unset ES_HOSTS or "
+ "ES_AWS_DOMAIN to suppress this message.", hosts, domain));
}
if (aws.getRegion() != null) {
return aws.getRegion();
} else if (domain != null) {
return new DefaultAwsRegionProviderChain().getRegion();
} else {
String awsRegion = regionFromAwsUrls(hosts);
checkArgument(awsRegion != null, "Couldn't awsRegion in '%s'", hosts);
return awsRegion;
}
}
/** By default, get credentials from the {@link DefaultAWSCredentialsProviderChain} */
@Bean
@ConditionalOnMissingBean
AWSCredentials.Provider credentials() {
return new AWSCredentials.Provider() {
AWSCredentialsProvider delegate = new DefaultAWSCredentialsProviderChain();
@Override public AWSCredentials get() {
com.amazonaws.auth.AWSCredentials result = delegate.getCredentials();
String sessionToken = result instanceof AWSSessionCredentials
? ((AWSSessionCredentials) result).getSessionToken()
: null;
return new AWSCredentials(
result.getAWSAccessKeyId(),
result.getAWSSecretKey(),
sessionToken
);
}
};
}
/**
* When the domain variable is set, we lookup the elasticsearch domain url dynamically, using the
* AWS api. Otherwise, we assume the URL specified in ES_HOSTS is correct.
*/
@Bean
@Conditional(AwsDomainSetCondition.class)
StorageComponent storage(
ZipkinElasticsearchHttpStorageProperties es,
ZipkinElasticsearchAwsStorageProperties aws,
@Qualifier("zipkinElasticsearchHttp") OkHttpClient client,
@Value("${zipkin.storage.strict-trace-id:true}") boolean strictTraceId) {
String domain = aws.getDomain();
String region = region(es, aws);
ElasticsearchDomainEndpoint hosts = new ElasticsearchDomainEndpoint(
client, HttpUrl.parse("https://es." + region + ".amazonaws.com"), domain);
return es.toBuilder(client).strictTraceId(strictTraceId).hostsSupplier(hosts).build();
}
static final class AwsDomainSetCondition extends SpringBootCondition {
static final String PROPERTY_NAME = "zipkin.storage.elasticsearch.aws.domain";
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata a) {
String domain = context.getEnvironment().getProperty(PROPERTY_NAME);
return domain == null || domain.isEmpty() ?
ConditionOutcome.noMatch(PROPERTY_NAME + " isn't set") :
ConditionOutcome.match();
}
}
static final class AwsMagic implements Condition {
@Override public boolean matches(ConditionContext condition, AnnotatedTypeMetadata md) {
String hosts = condition.getEnvironment().getProperty("zipkin.storage.elasticsearch.hosts");
String domain = condition.getEnvironment()
.getProperty("zipkin.storage.elasticsearch.aws.domain");
// If neither hosts nor domain, no AWS magic
if (isEmpty(hosts) && isEmpty(domain)) return false;
// Either we have a domain, or we check the hosts auto-detection magic
return !isEmpty(domain) || regionFromAwsUrls(Arrays.asList(hosts.split(","))) != null;
}
}
static String regionFromAwsUrls(List<String> hosts) {
String awsRegion = null;
for (String url : hosts) {
Matcher matcher = AWS_URL.matcher(url);
if (matcher.find()) {
String matched = matcher.group(1);
checkArgument(awsRegion == null,
"too many regions: saw '%s' and '%s'", awsRegion, matched);
awsRegion = matcher.group(1);
} else {
checkArgument(awsRegion == null,
"mismatched regions; saw '%s' but no awsRegion found in '%s'", awsRegion,
url);
}
}
return awsRegion;
}
private static boolean isEmpty(String s) {
return s == null || s.isEmpty();
}
}